package com.wontube.smr.bo.media;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.wontube.smr.pub.IObjectListener;



public final class OnlineMp3Converter {
	private static final Logger logger = LoggerFactory.getLogger(OnlineMp3Converter.class);
	
	// 跳过数据大于多少时重新连接
	private static final long minSkipBytes = 8192L;

	private final String flvUrl;
	private final String outputBase;
	// 视频流总长度
	private final long mediaLength;
	// 服务器是否支持分段
	private final boolean allowRange;

	// 数据偏移量,　相对于视频总长度
	private long streamOffset;
	private InputStream inputStream;
	private double percentWeight = 100;
	
	private IAudioWriter audioWriter;
	private boolean extractedAudio;
	private List<String> warnings;
	private HttpURLConnection request;
	
	// 监听者
	private ArrayList<IObjectListener<Double>> listeners = new ArrayList<IObjectListener<Double>>();

	public OnlineMp3Converter(String flvUrl, String outputBase) throws IOException {
		this.flvUrl = flvUrl;
		this.outputBase = outputBase;
		this.warnings = new ArrayList<String>();

		this.streamOffset = 0;
		this.request = this.createConnection(this.streamOffset);
		this.mediaLength = this.request.getContentLength();
		this.allowRange = (this.request.getResponseCode() == HttpURLConnection.HTTP_PARTIAL);
	}
	
	public void addListener(IObjectListener<Double> listener) {
		this.listeners.add(listener);
	}
	
	public void removeListener(IObjectListener<Double> listener) {
		this.listeners.remove(listener);
	}
	
	public File getOutputFile() {
		if (this.audioWriter == null) {
			return null;
		}
		return this.audioWriter.getFile();
	}

	public List<String> getWarnings() {
		return this.warnings;
	}

	public boolean getExtractedAudio() {
		return this.extractedAudio;
	}
	
	public double getPercentWeight() {
		return this.percentWeight;
	}
	
	public void setPercentWeight(double value) {
		this.percentWeight = value;
	}

	public boolean extractStreams() throws IOException {
		//this.seek(0);
		if (this.readUInt32() != 0x464C5601) { // not FLV file..
			logger.debug("Not flv file, impossible to extract addio track");
			return false;
		}
		
		this.readUInt8();
		long dataOffset = this.readUInt32();
		
		this.skip(dataOffset - this.streamOffset);
		this.readUInt32();

		double lastProgress = 0;
		double progress = 0;
		while (this.streamOffset < this.mediaLength) {
			if (!this.readTag()) {
				break;
			}

			if ((this.mediaLength - this.streamOffset) < 4) {
				break;
			}

			this.readUInt32();
			
			progress = this.streamOffset * this.percentWeight / this.mediaLength;
			if (progress >= lastProgress + 2) {
				this.reportProgress(progress);
				lastProgress = progress;
			}
		}

		if (this.audioWriter != null) {
			this.audioWriter.finish();
		}

		progress = this.streamOffset * this.percentWeight / this.mediaLength;
		this.reportProgress(progress);
		
		return true;
	}
	
	public void destroy() {
		if (this.inputStream != null) {
			try {
				this.inputStream.close();
			} catch (IOException e) {
				logger.error(e.toString());
			} finally {
				this.inputStream = null;
			}
		}
		if (this.request != null) {
			this.request.disconnect();
		}
	}
	
	private HttpURLConnection createConnection(long beginRange) throws IOException {
		this.destroy();
		URL uri = new URL(this.flvUrl);
		HttpURLConnection request = (HttpURLConnection) uri.openConnection();
		request.setConnectTimeout(20000);
		request.setReadTimeout(20000);
		request.setUseCaches(false);
		request.setRequestProperty("Range", "bytes=" + beginRange + "-");
		request.setRequestProperty("Connection", "Keep-Alive");
		
		request.connect();
		this.inputStream = request.getInputStream();

		return request;
	}
	
	private void reportProgress(double progress) {
		for (IObjectListener<Double> listener : this.listeners) {
			listener.onMessage(progress);
		}
	}

	private boolean readTag() throws IOException {
		if (this.mediaLength - this.streamOffset < 11) {
			return false;
		}

		// Read tag header
		long tagType = this.readUInt8();
		long dataSize = this.readUInt24();
		long timeStamp = this.readUInt24();
		timeStamp |= this.readUInt8() << 24;
		// streamID
		this.readUInt24();
		// Read tag data
		if (dataSize == 0) {
			return true;
		}

		if ((this.mediaLength - this.streamOffset) < dataSize) {
			return false;
		}

	    int	mediaInfo = this.readUInt8();
		dataSize -= 1;

		if (tagType == 0x8) { // Audio
			byte[] data = this.readBytes((int) dataSize);
			
			if (this.audioWriter == null) {
				this.audioWriter = this.getAudioWriter(mediaInfo);
				this.extractedAudio = audioWriter != null;
			}
			this.audioWriter.writeChunk(data, timeStamp);
		}
//		else if ((tagType == 0x9) && ((mediaInfo >> 4) != 5)) { // Video
//			this.skip(dataSize);
//			log.debug("skiped vedio size = " + dataSize);
//		}
		else {
			//log.debug("skiped size = " + dataSize);
			this.skip(dataSize);
		}

		return true;
	}

	private IAudioWriter getAudioWriter(int mediaInfo) throws IOException {
		int format = mediaInfo >> 4;
		// int rate = (mediaInfo >> 2) & 0x3;
		// int bits = (mediaInfo >> 1) & 0x1;
		// int chans = mediaInfo & 0x1;
		if ((format == 2) || (format == 14)) { // MP3
			String path = this.outputBase + ".mp3";
			return new MP3Writer(path, this.warnings);
		} else if (format == 10) { // AAC
			String path = this.outputBase + ".aac";
			return new AACWriter(path);
		} else {
			String typeStr;

			if (format == 1)
				typeStr = "ADPCM";
			else if ((format == 4) || (format == 5) || (format == 6)) {
				typeStr = "Nellymoser";
			} else {
				typeStr = "format=" + format;
			}
			
			this.warnings.add("Unable to extract audio (" + typeStr + " is unsupported).");
		}
		
		return null;
	}

	private void skip(long offset) throws IOException {
		this.streamOffset += offset;
		if (offset > minSkipBytes && this.allowRange) {
			this.createConnection(this.streamOffset);
		} else {
			this.inputStream.skip(offset);
		}
	}

	private int readUInt8() throws IOException {
		this.streamOffset++;
		return this.inputStream.read();
	}

	private long readUInt24() throws IOException {
		byte[] x = new byte[4];
		this.readBytes(x, 1, 3);
		
		return BitConverterBE.toUInt32(x, 0);
	}

	private long readUInt32() throws IOException {
		byte[] x = new byte[4];
		this.readBytes(x, 0, x.length);

		return BitConverterBE.toUInt32(x, 0);
	}

	private byte[] readBytes(int length) throws IOException {
		byte[] buff = new byte[length];
		this.readBytes(buff, 0, length);

		return buff;
	}
	
	private void readBytes(byte[] buffer, int offset, int length) throws IOException {
		int len = 0;
		int left = length;
		do {
			len = this.inputStream.read(buffer, offset, left);
			offset += len;
			left -= len;
		} while (len > 0 && left > 0);
		this.streamOffset += length;
	}
}
